declare const global: NodeJS.Global

export namespace utils {

  export function listContains<T>(list: T[], item: T): boolean {
    return list.indexOf(item) !== -1
  }

  /** 将类似 "SomeName" 的字符串转换成类似 "someName" 的形式 */
  export function pascalToCamel(str: string) {
    return str[0].toLowerCase() + str.slice(1)
  }

  /** 将类似 "someName" 的字符串转换成类似 "SomeName" 的形式 */
  export function camelToPascal(str: string) {
    return str[0].toUpperCase() + str.slice(1)
  }

  /** 从 url 中获取 path 的部分 */
  export function getPathFromUrl(url: string) {
    let s = url.replace(/^https?:\/\/[^?/]*/, '').split('#')[0]
    if (s.startsWith('?')) {
      s = '/' + s
    }
    return s
  }

  /**
   * 拷贝一个对象
   * @param source 源对象
   * @param deepClone 是否深拷贝。非深拷贝模式下只会拷贝一层
   * @param deepth 内部变量，记录当前已拷贝层数，用于环形数据结构检查，不需要手动设置
   */
  export function clone<T>(source: T, deepClone = false, deepth = 0): T {
    if (deepth > 100) {
      console.error('utils.clone 到达最大深度，放弃克隆。请检查是否存在环形数据结构')
      return source
    }
    if (source === null || source === undefined) {
      return source
    }
    if (source instanceof Array) {
      const res = []
      for (const item of source as any as any[]) {
        if (deepClone) {
          res.push(utils.clone(item, deepClone, deepth + 1))
        } else {
          res.push(item)
        }
      }
      return res as any
    } else if (typeof source === 'object') {
      const res = {} as any
      for (const key of Object.keys(source)) {
        if (deepClone) {
          res[key] = utils.clone(source[key as keyof typeof source], deepClone, deepth + 1)
        } else {
          res[key] = source[key as keyof typeof source]
        }
      }
      return res
    } else {
      return source
    }
  }

  export function pick<T, KS extends keyof T>(object: T, keys: KS[]): { [K in KS]: T[K] } {
    const res: any = {}
    for (const key of keys) {
      res[key] = object[key]
    }
    return res
  }

  /**
   * convert a string into camelcase
   * any charactor followed a separator will be convert to uppercased letter,
   * otherwise convert to lowercased letter
   * @param key string to convert
   * @param separators list of separator, defauls to "-"/"_"
   */
  export function camelizeKey(key: string, separators: string[] = ['-', '_']): string {
    const out: any = []
    let i: number = 0
    while (i < key.length) {
      if (separators.indexOf(key[i]) > -1) {
        out.push(key[i + 1].toUpperCase())
        i++
      } else {
        out.push(key[i])
      }
      i++
    }
    return out.join('')
  }

  /**
   * convert all keys in an object recursivly using utils.camelizeKey
   * @param obj
   */
  export function camelize(obj: object): { [key: string]: any } {
    if (obj === null || obj === undefined) {
      return obj
    } else if (obj instanceof Array) {
      return obj.map((item) => {
        return utils.camelize(item)
      })
    } else if (typeof obj === 'object') {
      const out: any = {}
      for (const key in (obj as any)) {
        const v = (obj as any)[key]
        out[utils.camelizeKey(key)] = utils.camelize(v)
      }
      return out
    } else {
      return obj
    }
  }

  /**
   * convert a string into words split by middlescore
   * - "someName" will be convert to "some-name"
   * - "SomeName" will be convert to "some-name" if ignoreFirst is true,
   *   otherwise it will be "-some-name"
   * @param key
   * @param ignoreFirst
   */
  export function middlelizeKey(key: string, ignoreFirst: boolean = true): string {
    const out: string[] = []
    let i: number = 0
    const lowerCasedStr = key.toString().toLowerCase()
    while (i < key.length) {
      if (key[i] !== lowerCasedStr[i]) {
        if (!ignoreFirst || i !== 0) {
          out.push('-')
          out.push(lowerCasedStr[i])
          i++
          continue
        }
      }
      out.push(key[i].toLocaleLowerCase())
      i++
    }
    return out.join('')
  }

  /**
   * convert all keys in object to middlescore-splitted key
   * - "someName" will be convert to "some-name"
   * - "SomeName" will be convert to "some-name" if ignoreFirst is true,
   *   otherwise it will be "-some-name"
   * @param obj target object
   */
  export function middlelize(obj: object): { [key: string]: any } {
    if (obj === null || obj === undefined) {
      return obj
    } else if (obj instanceof Array) {
      return obj.map((item) => {
        return utils.middlelize(item)
      })
    } else if (typeof obj === 'object') {
      const out: any = {}
      for (const key in (obj as any)) {
        const v = (obj as any)[key]
        out[utils.middlelizeKey(key)] = utils.middlelize(v)
      }
      return out
    } else {
      return obj
    }
  }

  /**
   * convert all keys in object to underline-splitted key
   * - "someName" will be convert to "some_name"
   * - "SomeName" will be convert to "some_name" if ignoreFirst is true,
   *   otherwise it will be "_some_name"
   * @param obj target object
   */
  export function underlize(obj: object): { [key: string]: any } {
    if (obj === null || obj === undefined) {
      return obj
    } else if (obj instanceof Array) {
      return obj.map((item) => {
        return utils.underlize(item)
      })
    } else if (typeof obj === 'object') {
      const out: any = {}
      for (const key in obj as any) {
        const v = (obj as any)[key]
        out[utils.underlizeKey(key)] = utils.underlize(v)
      }
      return out
    } else {
      return obj
    }
  }

  /**
   * convert key to be underline-splitted key
   * - "someName" will be convert to "some_name"
   * - "SomeName" will be convert to "some_name" if ignoreFirst is true,
   *   otherwise it will be "_some_name"
   * @param key
   * @param ignoreFirst
   */
  export function underlizeKey(key: string, ignoreFirst: boolean = false): string {
    const out: string[] = []
    let i: number = 0
    const lowerCasedStr: string = key.toString().toLowerCase()
    while (i < key.length) {
      if (key[i] !== lowerCasedStr[i]) {
        if (!ignoreFirst || i !== 0) {
          out.push('_')
          out.push(lowerCasedStr[i])
          i++
          continue
        }
      }
      out.push(key[i].toLocaleLowerCase())
      i++
    }
    return out.join('')
  }

  export function generateId() {
    return Date.now().toString(36) + parseInt(Math.random() * 1000000 + '').toString(36)
  }

  export function isEmptyObject(object: {}): boolean {
    if (typeof object !== 'object') {
      return false
    }
    // return Object.keys(object).length > 0
    for (const _ in object) {
      return false
    }
    return true
  }

  export function isEmptyArray(arr: any): boolean {
    if (arr instanceof Array) {
      return arr.length === 0
    }
    return false
  }

  /** returns a promise that resolves after specified time */
  export function asyncDelay(time: number) {
    return new Promise((resolve) => {
      setTimeout(() => {
        resolve()
      }, time)
    })
  }

  /**
   * parse url and get all query arguments
   * not available in wxapp
   * @param url
   */
  export function getUrlArgs(url: string, camelizeParamKeys = false): { [key: string]: string } {
    // remove url hash
    const [href] = url.split('#')
    const s = href.split('?')[1]
    if (!s) {
      return {}
    }
    let args: {
      [key: string]: string
    } = {}
    for (const a of s.split('&')) {
      const part = a.split('=')
      let _s: string
      try {
        _s = decodeURIComponent(part[1])
      } catch (error) {
        _s = part[1]
      }
      args[part[0]] = _s
    }
    if (camelizeParamKeys) {
      args = utils.camelize(args)
    }
    return args
  }

  /**
   * 生成URL链接
   * @param path URL中的路径
   * @param queryArgs URL中 “?” 后面的参数
   */
  export function genApiUrl(path: string, queryArgs: object): string {
    if (!queryArgs) {
      queryArgs = {}
    }
    const args: string[] = []
    for (const key in queryArgs as any) {
      const v = (queryArgs as any)[key]
      if (v !== null) {
        args.push(`${key}=${v}`)
      }
    }
    if (args.length === 0) {
      return path
    } else {
      return `${path}?${args.join('&')}`
    }
  }

  /**
   * 将指定时间戳转换成类似 ”5分钟前“ 这样的形式
   * @param time unix时间戳或Date对象，如果传入时间戳可以m秒为单位，或者毫秒为单位
   * @param detailed 输出简略格式或者详细格式，默认简略
   */
  export function timeFromNow(time: Date | number, detailed: boolean = false): string {
    let dateTime: Date
    if (time instanceof Date) {
      dateTime = time
    } else {
      dateTime = new Date(utils.normalizeTime(time))
    }
    const now = new Date().getTime()
    const d = now - dateTime.getTime()
    const thatday = utils.parseTime(dateTime, 'YMD')
    const today = utils.parseTime(now, 'YMD')
    const yesterday = utils.parseTime(now - 24 * 3600 * 1000, 'YMD')
    if (dateTime.getFullYear() !== new Date().getFullYear()) {
      // 不是今年
      if (detailed) {
        return utils.parseTime(dateTime, 'Y-M-D h:i')
      } else {
        return utils.parseTime(dateTime, 'M月D日')
      }
    }
    if (thatday !== today && thatday !== yesterday) {
      // 昨天以前
      if (detailed) {
        return utils.parseTime(dateTime, 'Y-M-D h:i')
      } else {
        return utils.parseTime(dateTime, 'M月D日')
      }
    }
    if (thatday === yesterday) {
      return `昨天${utils.parseTime(dateTime, 'h:i')}`
    }
    if (d > 3600 * 1000) {
      return `${Math.floor(d / 1000 / 60 / 60)}小时前`
    }
    if (d > 60 * 1000) {
      return `${Math.floor(d / 1000 / 60)}分钟前`
    }
    return '刚刚'
  }

  /**
   * 将一个时间对象和时间戳转换成对应的时间格式
   * @param time 时间对象和时间戳
   * @param format 时间格式, 参数：y年m月d日h点i分s秒c毫秒，w=weekDay，YMD分别是年月日的简写，
   */
  export function parseTime(time: Date | number, format = 'Y年M月D日h点i分'): string {
    // keys = "Y","M","D","d","h","m","s","c"
    if (!time) { return '' }
    if (typeof time === 'number') {
      time = utils.normalizeTime(time)
    }
    const arr: any = format.split('')
    const date = new Date(time as any)
    for (let index = 0; index < arr.length; index++) {
      const value = arr[index]
      switch (value) {
        case 'Y': arr[index] = date.getFullYear() - 2000; break
        case 'M': arr[index] = date.getMonth() + 1; break
        case 'D': arr[index] = date.getDate(); break
        case 'y': arr[index] = date.getFullYear(); break
        case 'm':
          const m: number = date.getMonth() + 1
          arr[index] = m < 10 ? `0${m}` : m.toString()
          break
        case 'd':
          const d: number = date.getDate()
          arr[index] = d < 10 ? `0${d}` : d.toString()
          break
        case 'w':
          const w: number = date.getDay()
          switch (w) {
            case 1: arr[index] = '一'; break
            case 2: arr[index] = '二'; break
            case 3: arr[index] = '三'; break
            case 4: arr[index] = '四'; break
            case 5: arr[index] = '五'; break
            case 6: arr[index] = '六'; break
            case 0: arr[index] = '日'; break
          }
          break
        case 'h':
          arr[index] = date.getHours().toString()
          while (arr[index].length < 2) {
            arr[index] = '0' + arr[index]
          }
          break
        case 'i':
          arr[index] = date.getMinutes().toString()
          while (arr[index].length < 2) {
            arr[index] = '0' + arr[index]
          }
          break
        case 's':
          arr[index] = date.getSeconds().toString()
          while (arr[index].length < 2) {
            arr[index] = '0' + arr[index]
          }
          break
        case 'c':
          arr[index] = date.getMilliseconds().toString()
          while (arr[index].length < 3) {
            arr[index] = '0' + arr[index]
          }
          break
      }
    }
    return arr.join('')
  }

  /**
   * 用于统一毫秒单位时间戳和秒单位时间戳
   * @param time 时间戳
   * @param toSecond 最终计算出来是否以秒为单位
   */
  export function normalizeTime(time: number, toSecond: boolean = false): number {
    time = time * 1
    if (toSecond) {
      if (time > (new Date('3000-01-01').getTime() / 1000)) {
        time = time / 1000
      }
    } else {
      if (time < (new Date('1971-01-01').getTime())) {
        time = time * 1000
      }
    }
    return time
  }

  /**
   * 生成随机字符串
   */
  export function genSessionId(): string {
    return (
      Date.now().toString(36) +
      parseInt(Math.random() * 100000 + '').toString(36)
    )
  }

  /**
   * 或操作，类似操作符 “||” ，但是只有 null 和 undefin 的时候会命中， 0 或 false 时不会
   */
  export function or<T>(value1: T, value2: T): T {
    return value1 !== undefined && value1 !== null ? value1 : value2
  }

  /**
   * 将键值对转换成列表
   * @param map 键值对map
   */
  export function mapToList<T>(map: { [key: string]: T }): T[] {
    const res: T[] = []
    for (const key in map) {
      res.push(map[key])
    }
    return res
  }

  /**
   * 向给定 url 后添加 query 参数
   */
  export function addUrlArgs(
    url: string,
    args: { [key: string]: string | number }
  ): string {
    const res = url.split('#')
    let baseUrl = res[0]
    const hash = res[1]
    const parts: string[] = []
    for (const k in args) {
      const v = args[k]
      if (v !== undefined && v !== null) {
        parts.push(k + '=' + encodeURIComponent(v.toString()))
      }
    }
    if (baseUrl.indexOf('?') > -1) {
      baseUrl += '&' + parts.join('&')
    } else {
      baseUrl += '?' + parts.join('&')
    }
    if (hash) {
      return baseUrl + '#' + hash
    } else {
      return baseUrl
    }
  }

  /**
   * 生成距离现在给定天数时的时间，格式可以通过 format 参数定制，规则与 parseTime 相同
   * @param offset 时间偏移量
   * @param format 时间格式
   */
  export function getTimeByOffset(offset: number, format = 'y-m-d'): string {
    const t = Date.now() + offset * (24 * 3600 * 1000)
    return utils.parseTime(t, format)
  }

  type MaxRetryHandler = (error: any) => boolean
  interface RetryUntilResolveRes<T> {
    handleMaxRetry: (handler: MaxRetryHandler) => RetryUntilResolveRes<T>
    toPromise: () => Promise<T>
  }

  export function retryUntilResolve<T>(
    handler: () => Promise<T>,
    retryLimit: number
  ): RetryUntilResolveRes<T> {
    let retryCount = 0
    let maxRetryHandler: MaxRetryHandler
    let p = new Promise<T>(function (resolve, reject) {
      function iter() {
        console.log(`call iter, ${retryCount}/${retryLimit}`)
        p = handler()
        if (!(p instanceof Promise)) {
          console.error(
            'utils.retryUntilResolve only accept handler that return Promise instance'
          )
          reject()
        }
        p.then(function (data) {
          resolve(data)
        })
        p.catch(function (e) {
          if (e === true) {
            // 上级handler使用reject(true)可以阻止默认重试行为
            return reject({ userAbort: true })
          }
          if (retryCount < retryLimit) {
            retryCount += 1
            return iter()
          }
          // TODO consider support async handler
          if (maxRetryHandler && maxRetryHandler(e) === true) {
            // 如果 maxRetryHandler 返回 true，则重新尝试
            retryCount += 0
            return iter()
          }
          reject({ userAbort: false })
        })
      }
      iter()
    })
    const res: RetryUntilResolveRes<T> = {
      handleMaxRetry(
        handlerFn: MaxRetryHandler
      ): RetryUntilResolveRes<T> {
        maxRetryHandler = handlerFn
        return res
      },
      toPromise(): Promise<T> {
        return p
      }
    }
    return res
  }

  export function unicodeToChar(text: string) {
    return text.replace(/\\u[\dA-F]{4}/gi, function (match) {
      return String.fromCharCode(parseInt(match.replace(/\\u/g, ''), 16))
    })
  }

  export function parseCurrency(fenPrice: number, detailed = false): string {
    const yuanPrice = fenPrice / 100
    return utils.numberToFixed(yuanPrice, 2, detailed)
  }

  export function numberToFixed(
    num: number,
    fixedLen: number,
    forceFiexed = false
  ): string {
    if (isNaN(num)) {
      return 'NaN'
    }
    const s = num.toString()
    const [i, f = ''] = s.split('.')
    let res = `${i}.${f.slice(0, fixedLen)}`
    if (f.length < fixedLen && forceFiexed) {
      for (let j = 1; j <= fixedLen - f.length; j++) {
        res += '0'
      }
    }
    if (res[res.length - 1] === '.') {
      res = res.slice(0, -1)
    }
    return res
  }

  export function random(a: number, b: number, returnInt = false) {
    let d = Math.random() * (b - a)
    if (returnInt) {
      d = Math.round(d)
    }
    return a + d
  }

  /** 将url中的 http: 换成 https: */
  export function secureUrl(url: string) {
    return url.replace(/^http:/, 'https:')
  }

  /**
   * 计算时间差，放回相差多少年，多少月，多少天，多少小时，多少分钟
   * @param start 开始时间
   * @param end 结束时间
   */
  export function formatSpecificOffsetTime(start: Date | number,
                                           end: Date | number)
    : { year: number, month: number, days: number, hours: number, minutes: number } {
    const yearLevelValue = 365
    // 一个月按 30 天算
    const monthLevelValue = 30
    const dayLevelValue = 24 * 60 * 60 * 1000
    const hourLevelValue = 60 * 60 * 1000
    const minuteLevelValue = 60 * 1000
    // const secondLevelValue = 1000;
    let datetimeStart: Date
    let datetimeEnd: Date
    if (start instanceof Date) {
      datetimeStart = start
    } else {
      datetimeStart = new Date(utils.normalizeTime(start))
    }
    if (end instanceof Date) {
      datetimeEnd = end
    } else {
      datetimeEnd = new Date(utils.normalizeTime(end))
    }
    const period = datetimeEnd.getTime() - datetimeStart.getTime()
    if (period <= 0) {
      return { year: 0, days: 0, hours: 0, minutes: 0, month: 0 }
    }
    // 计算出相差天数
    const totalDays = Math.floor(period / dayLevelValue)
    // 计算出小时数
    const hours = Math.floor(period % dayLevelValue / hourLevelValue)
    const minutes = Math.floor(period % dayLevelValue % hourLevelValue / (minuteLevelValue))
    const year = totalDays >= yearLevelValue ? Math.floor(totalDays / yearLevelValue) : 0
    const month = totalDays >= yearLevelValue
      ? Math.floor((totalDays - year * yearLevelValue) / monthLevelValue)
      : totalDays >= monthLevelValue ? Math.floor(totalDays / monthLevelValue) : 0
    const days = totalDays - month * 30 - year * yearLevelValue
    return { year, month, days, hours, minutes }
  }

  /**
   * 获取当前月份的天数
   * @param year number
   */
  export function getDays(year: number): number {
    const date = new Date()
    const mouth = date.getMonth() + 1// 获取当前月份
    let days// 定义当月的天数；
    if (mouth === 2) {// 当月份为二月时，根据闰年还是非闰年判断天数
      days = year % 4 === 0 ? 29 : 28
    } else if (
      mouth === 1 || mouth === 3 || mouth === 5 || mouth === 7 || mouth === 8 || mouth === 10 || mouth === 12) {
      // 月份为：1,3,5,7,8,10,12 时，为大月.则天数为31；
      days = 31
    } else {
      // 其他月份，天数为：30.
      days = 30
    }
    return days
  }

  // Browser environment sniffing
  export function getUserAgent(ua: string): 'IE' | 'IE9' | 'isEdge' | 'android' | 'IOS' | 'chrome' {
    const UA = ua
    const isIE = UA && /msie|trident/.test(UA)
    if (isIE) {
      return 'IE'
    }
    const isIE9 = UA && UA.indexOf('msie 9.0') > 0
    if (isIE9) {
      return 'IE9'
    }
    const isEdge = UA && UA.indexOf('edge/') > 0
    if (isEdge) {
      return 'isEdge'
    }
    const isAndroid = (UA && UA.indexOf('android') > 0)
    if (isAndroid) {
      return 'android'
    }
    const isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA))
    if (isIOS) {
      return 'IOS'
    }
    const isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge
    if (isChrome) {
      return 'chrome'
    }
    return 'chrome'
  }

  /**
   * Check if val is a valid array index.
   */
  export function isValidArrayIndex(val: any): boolean {
    const n = parseFloat(String(val))
    return n >= 0 && Math.floor(n) === n && isFinite(val)
  }

  const _toString = Object.prototype.toString
  /**
   * Strict object type check. Only returns true
   * for plain JavaScript objects.
   */
  export function isPlainObject(obj: any): boolean {
    return _toString.call(obj) === '[object Object]'
  }

  export function isRegExp(v: any): boolean {
    return _toString.call(v) === '[object RegExp]'
  }

  export function toNumber(val: string): number | string {
    const n = parseFloat(val)
    return isNaN(n) ? val : n
  }

  /**
   * Remove an item from an array.
   */
  export function removeItem(arr: any[], item: any): any[] | void {
    if (arr.length) {
      const index = arr.indexOf(item)
      if (index > -1) {
        return arr.splice(index, 1)
      }
    }
  }

  /**
   * Check whether an object has the property.
   */
  const hasOwnProperty = Object.prototype.hasOwnProperty
  export function hasOwn(obj: object | any[], key: string): boolean {
    return hasOwnProperty.call(obj, key)
  }

  export function toArray(list: any, start?: number): any[] {
    start = start || 0
    let i = list.length - start
    const ret: any[] = new Array(i)
    // 倒序遍历
    while (i--) {
      ret[i] = list[i + start]
    }
    return ret
  }

  /**
   * Ensure a function is called only once.
   */
  export function onceCallFn(fn: Function): Function {
    let called = false
    return function () {
      if (!called) {
        called = true
        fn.apply(global, arguments)
      }
    }
  }

  /**
   * 合并两个对象
   * @param first 第一个对象
   * @param second  第二个对象
   */
  export function mergeObject<T extends object, U extends object>(first: T, second: U): T & U {
    const result = {} as T & U
    for (const id in first) {
      (result as T)[id] = first[id]
    }
    for (const id in second) {
      if (!result.hasOwnProperty(id)) {
        (result as U)[id] = second[id]
      }
    }

    return result
  }

  /**
   * 将数组转换为 元组类型
   * @param v
   */
  export function tuple<U extends any[]>(...v: U[]) {
    return v
  }

  /**
   * 保持对象字面量的声明
   * 避免 eg: const b = {a : "xxx"} => b: {a: string}, 而保持字面量的类型约束， {a: "xxx"}
   * @param v
   */
  export function raw<T extends string | number | symbol>(v: T) {
    return v
  }

  export function isFunction(fn: any) {
    return fn && {}.toString.call(fn) === '[object Function]'
  }

  export function isConstructor(f: any) {
    try {
      new f()
    } catch (err) {
      return false
    }
    return true
  }
}

export default utils
